// TODO: CODE REVIEW & CLEANUP!
//
// SyncServer.cs
//
// Implements SyncServer and related types.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using HomeUX.DriverModel;
using HomeUX.Networking;
using System.Security.Cryptography;
using HomeUX.Synchronization;
using HomeUX.Utilities;
using System.IO.IsolatedStorage;
using System.Text.RegularExpressions;

namespace HomeUX.Server
{

public class SyncServer
{
    private PseudoClientImplementation PseudoClientImpl { get; set; }

    /// <summary>
    /// The server-side pseudo-client.
    /// </summary>
    public ISyncClient PseudoClient
    {
        get
        {
            return PseudoClientImpl;
        }
    }

    public bool IsDeviceEnabled(string deviceName)
    {
        Driver device;
        if (!_devices.TryGetValue(deviceName, out device))
            return false;
        return device.IsEnabled;
    }

	WorkQueue _workQueue;
    string _password = null;

#if !SILVERLIGHT

    int _portNumber = 0;
    SocketServer _socketServer;

#endif

    // the "<HomeUXSettings>" XML element from the .config file
    internal ConfigurationElement Configuration { get; private set; }

    /// <summary>
    /// A string that identifies the application .config file for debugging purposes -- this may
    /// be the actual path to the file, or a generic name used to identify the file (in the case
    /// where the actual path is not known).
    /// </summary>
    internal string ConfigurationFileLabel { get; private set; }

    public string StorageDirectoryPath { get; private set; }

    // maps session ID to SyncClientAgent, which is the server-side representative of SyncClient on a client
    Dictionary<Guid, SyncClientAgent> _agents = new Dictionary<Guid, SyncClientAgent>();

    // maps device name to a Device (which is an instance of a driver)
    Dictionary<string, Driver> _devices = new Dictionary<string, Driver>();

    public ILogDestination LogDestination { get; set; }

    //public Guid[] SessionIds
    //{
    //    get
    //    {
    //        return _agents.Keys.ToArray();
    //    }
    //}

    /// <summary>
    /// Gets or sets the <r>WorkQueue</r> that this <r>SyncServer</r> will use to schedule
    /// asynchronous work items.  This property must be set to a valid <r>WorkQueue</r> instance
    /// before <r>Start</r> is called, and must be set to <n>null</n> after <r>Stop</r> is called.
    /// The caller owns the <r>WorkQueueInstance</r>, so the caller is also responsible for
    /// calling <r>WorkQueue.Dispose</r> after this property has been set to <n>null</n>.
    /// </summary>
    ///
	public WorkQueue WorkQueue
	{
		get
		{
			return _workQueue;
		}
		set
		{
			DisallowPropertySetAfterStart("WorkQueue");

            if (value != null)
            {
                value.ExceptionCaught += _workQueue_ExceptionCaught;
                value.CatchExceptionTypes(typeof(SyncException));
            }
            else
                _workQueue.ExceptionCaught -= _workQueue_ExceptionCaught;

            _workQueue = value;
        }
	}

    void _workQueue_ExceptionCaught(object sender, ExceptionCaughtEventArgs e)
    {
        string message = Util.FormatExceptionMessage(e.Exception);

        if (LogDestination != null)
            LogDestination.LogError(message);

        BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
        {
            messageBuilder.WriteAlertElement(MessageAlertType.Warning, message);
        });
    }

#if !SILVERLIGHT
	public int PortNumber
	{
		get
		{
			return _portNumber;
		}
		set
		{
			DisallowPropertySetAfterStart("PortNumber");
			_portNumber = value;
		}
	}
#endif

	public string Password
	{
		get
		{
			return _password;
		}
		set
		{
			DisallowPropertySetAfterStart("Password");
			_password = value;
		}
	}

    public void ReadConfigurationFile(TextReader textReader, string fileLabel)
    {
        ConfigurationFileLabel = fileLabel;
        var settings = new XmlReaderSettings();
        settings.IgnoreComments = true;
        settings.IgnoreProcessingInstructions = true;
        settings.IgnoreWhitespace = true;
        using (var reader = XmlReader.Create(textReader))
        {
            try
            {
                if (reader.MoveToContent() != XmlNodeType.Element)
                    throw new FatalException("Root element not found");
                if (!reader.ReadToFollowing("HomeUXSettings"))
                    throw new FatalException("<HomeUXSettings> element not found");
                Configuration = ReadConfigurationElement(reader, reader.LocalName, null);
            }
#if !NOCATCH
            catch (Exception ex)
            {
                if (!(ex is XmlException) && !(ex is FatalException))
                    throw;
                throw new FatalException("Error in configuration file \"{0}\", line {1}: {2}",
                    fileLabel, ((IXmlLineInfo)reader).LineNumber, ex.Message);
            }
#endif
            finally
            {
            }
        }
    }

    // on entry, <reader> is positioned on the element node; on exit, <reader> is positioned on
    // whatever node ended the element (so the next Read will position <reader> on the beginning
    // of the next node)
    // <elementName> is the name of the element, or, if the element is of the form "on-(computer)-(whatever)",
    // then <elementName> is "(whatever)"
    ConfigurationElement ReadConfigurationElement(XmlReader reader, string elementName,
        Dictionary<string,bool> conditions)
    {
        int lineNumber = ((IXmlLineInfo)reader).LineNumber;

        // special case: if <conditions> is null, this is the root "<HomeUXSettings>" element
        bool isRootElement = (conditions == null);
        if (isRootElement)
            conditions = new Dictionary<string, bool>();

        // read the attributes (if any)
        Dictionary<string, ConfigurationAttributeValue> attributes = null;
        string conditionsString = null;
        if (reader.MoveToFirstAttribute())
        {
            while (true)
            {
                string attributeName;
                if (attributes == null)
                    attributes = new Dictionary<string,ConfigurationAttributeValue>();
                bool conditionValue;
                if ((reader.NamespaceURI.Length > 0) && !isRootElement &&
                    (!conditions.TryGetValue(reader.Prefix, out conditionValue) ||
                     !conditionValue))
                {
                    // ignore attributes with a prefix that isn't specified in <conditions>
                }
                else
                if (ShouldIgnoreXmlNode(reader.LocalName, out attributeName))
                {
                    // ignore attributes whose name starts with "x-" -- this is a convenient way
                    // to "comment out" attributes -- or that start with "on-(computer)-" (if
                    // this computer isn't named "(computer)"
                }
                else
                if (isRootElement)
                {
                    // special case: this is the root "<HomeUXSettings>" element
                    if (attributeName == "StorageDirectory")
                        StorageDirectoryPath = reader.Value;
                    else
                    if (attributeName == "silverlight-Conditions")
					{
#if SILVERLIGHT
                        conditionsString = reader.Value;
#endif
					}
					else
                    if (attributeName == "Conditions")
					{
#if !SILVERLIGHT
						if (conditionsString != null)
						{
							throw new ConfigurationException(
								new ConfigurationElement(elementName, lineNumber, null, null, null),
								"Duplicate attribute \"{0}\"",
								attributeName);
						}
                        conditionsString = reader.Value;
#endif
					}
					else
                    if (reader.NamespaceURI == "http://www.w3.org/2000/xmlns/")
                        conditions[attributeName] = false;
                    else
                    {
                        throw new ConfigurationException(
                            new ConfigurationElement(elementName, lineNumber, null, null, null),
                            "Unknown attribute \"{0}\"",
                            attributeName);
                    }
                }
                else
                {
                    try
                    {
                        attributes.Add(attributeName,
                            new ConfigurationAttributeValue()
                            {
                                Value = reader.Value
                            });
                    }
                    catch (ArgumentException)
                    {
                        throw new ConfigurationException(
                            new ConfigurationElement(elementName, lineNumber, null, null, null),
                            "Attribute \"{0}\" is specified more than once -- check \"Conditions\" in <HomeUXSettings>",
                            attributeName);
                    }
                }
                if (!reader.MoveToNextAttribute())
                    break;
            }
            reader.MoveToElement();
        }

        if (isRootElement)
        {
            if (conditionsString != null)
            {
                foreach (string conditionPrefix in conditionsString.Split(';'))
                {
                    if (!conditions.ContainsKey(conditionPrefix))
                    {
                        throw new ConfigurationException(
                            new ConfigurationElement(elementName, lineNumber, null, null, null),
                            "Cannot specify Condition=\"{0}\" because xmlns:{0}=\"...\" was not specified",
                            conditionPrefix);
                    }
                    conditions[conditionPrefix] = true;
                }
            }
        }

        // read the text and child elements (if any)
        string text = null;
        List<ConfigurationElement> children = null;
        if (!reader.IsEmptyElement)
        {
            reader.Read();
            if ((reader.NodeType == XmlNodeType.Text) || (reader.NodeType == XmlNodeType.CDATA))
            {
                text = reader.Value;
                reader.Read();
            }
            while (true)
            {
                XmlNodeType nodeType = reader.MoveToContent();
                if (nodeType == XmlNodeType.EndElement)
                    break;
                else
                if (nodeType == XmlNodeType.Element)
                {
                    string childElementName;
                    bool conditionValue;
                    if ((reader.NamespaceURI.Length > 0) && !isRootElement &&
                        (!conditions.TryGetValue(reader.Prefix, out conditionValue) ||
                         !conditionValue))
                    {
                        // ignore elements with a prefix that isn't specified in <conditions>
                        reader.Skip();
                    }
                    else
                    if (ShouldIgnoreXmlNode(reader.LocalName, out childElementName))
                    {
                        // ignore elements whose names start with "x-" -- this is a convenient way
                        // to comment out stuff -- or that start with "on-(computer)-" (if this
                        // computer isn't named "(computer)"
                        reader.Skip();
                    }
                    else
                    {
                        if (text != null)
                            throw new FatalException("Cannot mix text with XML elements");
                        if (children == null)
                            children = new List<ConfigurationElement>();
                        children.Add(ReadConfigurationElement(reader, childElementName, conditions));
                    }
                }
                else
                if (text != null)
                    throw new FatalException("Cannot mix text with other XML node types");
                reader.Read();
            }
        }

        return new ConfigurationElement(elementName, lineNumber, attributes, text, children);
    }

    static readonly Regex OnComputerRegex = new Regex(@"^on-([^-]+)-(.+)$");


    /// <summary>
    /// Returns <n>true</n> if an XML element or attribute with a given name should be ignored,
    /// based on its name.
    /// </summary>
    ///
    /// <param name="localName">The name of the XML element or attribute, without any namespace prefix.</param>
    ///
    bool ShouldIgnoreXmlNode(string localName, out string modifiedLocalName)
    {
        // ignore this XML node if its name starts with "x-"
        if (localName.StartsWith("x-"))
        {
            modifiedLocalName = localName; // name won't be used
            return true;
        }

        Match match = OnComputerRegex.Match(localName);
        if (match.Success)
        {
            modifiedLocalName = match.Groups[2].Value;
            return !String.Equals(match.Groups[1].Value, Util.ComputerName,
                StringComparison.InvariantCultureIgnoreCase);
        }

        modifiedLocalName = localName;
        return false;
    }

	void InitializePseudoClient()
	{
        PseudoClientImpl = new PseudoClientImplementation(this);
        SyncClientAgent pseudoClientAgent = CreateAgent(PseudoClientImpl.ServerEndpointBase);
        PseudoClientImpl.Agent = pseudoClientAgent;
        QueuePersistDevicesState(true); // always save on driver start, even if no changes
	}

    WorkItem _persistDevicesStateWorkItem;

    void QueuePersistDevicesState(bool forceSave)
    {
        _persistDevicesStateWorkItem = _workQueue.Add(
#if SILVERLIGHT
            // the simulated server is a Silverlight application, which doesn't have the
            // opportunity to save state as it closes, so we work around this by saving
            // much more quickly
            500,
#else
            60000,
#endif
            delegate
            {
                PersistDevicesState(forceSave);
            });
    }

    void PersistDevicesState(bool forceSave)
    {
        QueuePersistDevicesState(false);

        foreach (Driver device in _devices.Values)
        {
            if (device.PersistedStateNeedsSaving || forceSave)
            {
                device.PersistedStateNeedsSaving = false;
                try
                {
                    device.PersistState();
                }
#if !NOCATCH
                catch (Exception ex)
                {
                    if ((ex is IOException) || (ex is MessageProtocolException)
#if SILVERLIGHT
                        || (ex is IsolatedStorageException)
#endif
                        )
                    {
                        if (LogDestination != null)
                        {
                            LogDestination.LogWarning(device.MessagePrefix + "Failed to persist state: {0}",
                                Util.FormatExceptionMessage(ex));
                        }
                    }
                    else
                        throw;
                }
#endif
                finally
                {
                }
            }
        }
    }

    public void ReadGeneralSettings()
    {
        var elements = new List<ConfigurationElement>(
            Configuration.Children.Where(e => e.Name == "GeneralSettings"));
        if (elements.Count != 1)
        {
            throw new ConfigurationException(Configuration,
                "There must be one <GeneralSettings> element; found {0}", elements.Count);
        }
        ConfigurationElement element = elements[0];


        StorageDirectoryPath = element.GetRequiredAttribute("StorageDirectory");
#if !SILVERLIGHT
        if (!Directory.Exists(StorageDirectoryPath))
        {
            throw new ConfigurationException(element,
                "Storage directory doesn't exist: {0}", StorageDirectoryPath);
        }
#endif
        
        // read these attributes even in the Silverlight case (where they're not used) --
        // otherwise we'll generate an "unknown attribute" error
        int portNumber = element.GetRequiredIntAttribute("PortNumber");
#if SILVERLIGHT
        string encryptedPassword = element.GetOptionalAttribute("EncryptedPassword", null);
#else
        string encryptedPassword = element.GetRequiredAttribute("EncryptedPassword");
#endif

        // set the port number and password
#if !SILVERLIGHT
        PortNumber = portNumber;
        try
        {
            Password = Util.UnprotectString(encryptedPassword, DataProtectionScope.LocalMachine);
        }
        catch (CryptographicException)
        {
            throw new ConfigurationException(element,
                "Attribute \"EncryptedPassword\" is improperly formatted, or the password wasn't encrypted on this computer");
        }
#endif

        // check for unknown elements and attributes
        element.CheckForExtraElementsAndAttributes();
    }

#if SILVERLIGHT
    public void Start()
    {
		InitializePseudoClient();
        InitializeBuiltinDrivers();
        //InitializeBuiltinAggregatorFunctions();
    }
#else
    /// <summary>
    /// Begins accepting incoming connections.
    /// </summary>
    /// 
    /// <exception cref="FatalException">
    /// The <r>SyncServer</r> could not be started.
    /// </exception>
    ///
    public void Start()
	{
        if (_workQueue == null)
            RequirePropertySetBeforeStart("WorkQueue");
        if (_portNumber == 0)
            RequirePropertySetBeforeStart("PortNumber");

        // continue initialization on the WorkQueue thread
        _workQueue.Add(Start2);
        _start2Event.WaitOne();
    }

    System.Threading.ManualResetEvent _start2Event = new System.Threading.ManualResetEvent(false);

    public void Start2()
    {
        try
        {
            // create device instances
            //var logMessage = new StringBuilder(1000);
            InitializePseudoClient();
            InitializeBuiltinDrivers();
            //InitializeBuiltinAggregatorFunctions();
            string constructionSpec;
            Driver device;
            foreach (ConfigurationElement element in Configuration.Children)
            {
                switch (element.Name)
                {

                case "GeneralSettings":

                    // ignore this element -- it's processed separately above
                    break;

                case "Device":

                    // can't check for extras here -- only driver knows what attributes are expected
                    string deviceName = element.GetRequiredAttribute("Name");
                    constructionSpec = element.GetRequiredAttribute("Driver");
                    try
                    {
                        device = Util.CreateInstance<Driver>(constructionSpec);
                    }
                    catch (CreateInstanceException ex)
                    {
                        throw new ConfigurationException(ex, element, "Failed to load driver: {0}",
                            constructionSpec);
                    }

                    try
                    {
                        var deviceParameters = CreateDeviceParameters(deviceName);
                        deviceParameters.Configuration = element;

                        if (LogDestination != null)
                        {
                            LogDestination.LogInfo("Device \"{0}\" (driver {1}) is initializing",
                                deviceName, device.GetType());
                        }

                        device.Initialize(deviceParameters);
                        AddDevice(device);
                    }
#if !NOCATCH
                    catch (Exception ex)
                    {
                        ex = ConfigurationException.ToFatalException(ex, ConfigurationFileLabel);
                        if ((ex is FatalException) || (ex is VirtualSerialPortException))
                        {
                            throw new FatalException(ex, "Error initializing device \"{0}\"",
                                deviceName);
                        }
                        else
                        {
                            if (LogDestination != null)
                            {
                                LogDestination.LogError(
                                    "Exception initializing device \"{0}\" -- server will exit.\r\n\r\n{1}",
                                    deviceName, ex);
                            }
                            throw;
                        }
                    }
#else
                    finally
                    {
                    }
#endif

                    //logMessage.AppendLine();
                    break;

                default:

                    throw new ConfigurationException(element, "Unknown element <{0}>", element.Name);

                }
            }
            //if (logMessage.Length == 0)
            //    logMessage.Append("No devices created");

            //if (LogDestination != null)
            //    LogDestination.LogInfo(logMessage.ToString());

            // start the socket server, which implements the HomeUX Sync protocol
            _socketServer = new SocketServer(_portNumber, _workQueue, 100, 100000);
            _socketServer.Connected += new ConnectedEventDelegate(_socketServer_Connected);
            _socketServer.DataReceived += new DataReceivedEventDelegate(_socketServer_DataReceived);
            _socketServer.Disconnected += new DisconnectedEventDelegate(_socketServer_Disconnected);
            _socketServer.Start(); // throws FatalException on error
        }
#if !NOCATCH
        catch (Exception ex)
        {
            ex = ConfigurationException.ToFatalException(ex, ConfigurationFileLabel);
            if (ex is FatalException)
            {
                if (LogDestination != null)
                    LogDestination.LogError(Util.FormatExceptionMessage(ex));
                Environment.Exit(1);
            }
            else
                throw;
        }
#endif
        finally
        {
            _start2Event.Set();
        }
	}

    void _socketServer_Connected(SocketConnection connection)
    {
        _workQueue.AssertIsWorkQueueThread();

        var endpoint = new ServerEndpoint(connection, _password, _workQueue);
        endpoint.MessageReceived += endpoint_MessageReceived;
        endpoint.IsConnectedChanged += endpoint_IsConnectedChanged;
        connection.Tag = endpoint;
        Util.Trace("SyncServer: Connected to: {0}", connection.ClientAddress);
    }

    void _socketServer_DataReceived(SocketConnection connection, byte[] buffer,
        int bytesReceived)
    {
        _workQueue.AssertIsWorkQueueThread();

        var endpoint = (ServerEndpoint)connection.Tag;
        endpoint.OnDataReceived(buffer, bytesReceived);
    }

    void _socketServer_Disconnected(SocketConnection connection)
    {
        _workQueue.AssertIsWorkQueueThread();

        var endpoint = (ServerEndpoint)connection.Tag;
        endpoint.Dispose();
        Util.Trace("SyncServer: Disconnected from: {0}", connection.ClientAddress);
    }

    void endpoint_IsConnectedChanged(object sender, EventArgs e)
    {
        _workQueue.AssertIsWorkQueueThread();

        var endpoint = (ServerEndpoint)sender;
        if (endpoint.IsConnected)
        {
            // A client has just connected.  Look for a SyncClientAgent with session ID =
            // <endpoint.sessionId>:
            //   -- If found, send the client sync tree deltas that were queued up since
            //      the connection was lost.
            //   -- If not found (e.g. (a) this is a new session, or (b) the old session's
            //      delta queue overflowed, causing the session to be deleted, or (c) the
            //      old session was deleted because we reached the maximum number of
            //      connected clients, etc.), create a new session and send the client the
            //      entire sync tree.

            SyncClientAgent agent = FindAgent(endpoint);
            if (agent == null)
            {
                agent = CreateAgent(endpoint);

                // send a message that initializes the client with the current state of the server
                agent.SendMessage(CreateClientFullUpdateMessage(endpoint.SessionId));
            }

            endpoint.Tag = agent;
        }
        else
        {
            // TODO: disconnected
        }
    }

    void endpoint_MessageReceived(object sender, MessageReceivedEventArgs e)
    {
        _workQueue.AssertIsWorkQueueThread();

        // retrieve event arguments
        var endpoint = (ServerEndpoint)sender;
        var agent = (SyncClientAgent)endpoint.Tag;
        Message message = e.Message;
        OnMessageReceived(agent, message);
    }
#endif

    public void Stop()
    {
#if !SILVERLIGHT
        PersistDevicesState(false);
        if (_socketServer != null)
        {
            _socketServer.Dispose();
            _socketServer = null;
        }
#endif

        foreach (Driver device in _devices.Values)
            device.Dispose();
        _devices.Clear();
        // note that the application is responsible for setting the WorkQueue property to null and
        // disposing of the WorkQueue

        //_workQueue.Dispose();
    }

    internal void InitializeBuiltinDrivers()
    {
        DeviceParameters deviceParameters;

        var personalization = new PersonalizationDriver();
        deviceParameters = CreateDeviceParameters("Personalization");
        personalization.Initialize(deviceParameters);
        AddDevice(personalization);

        var deviceStatus = new DeviceStatusDriver();
        deviceParameters = CreateDeviceParameters("DeviceStatus");
        deviceStatus.Initialize(deviceParameters);
        AddDevice(deviceStatus);
    }

    internal void OnMessageReceived(SyncClientAgent agent, Message message)
    {
        _workQueue.AssertIsWorkQueueThread();

        Util.TraceBlock(message.BodyAsString, "Server <-- Client#{0}", agent.Endpoint.ShortSessionId);

        // hack test
        //Console.WriteLine("Client#{0} --> {1} ({2} bytes)", agent.Endpoint.ShortSessionId, message.Name,
        //    ((message.Body != null) ? message.Body.Length : 0));

        // if the client sent a "Ping" message, just return the message contents back to the
        // client
        if (message.Name == "Ping")
        {
            // if a checksum was included, check it 
            string str;
            if (message.Headers.TryGetValue("Checksum", out str))
            {
                ulong sentChecksum = ulong.Parse(str);
                ulong checksum = 0;
                for (int i = 0; i < message.Body.Length; i++)
                    checksum = ((checksum << 8) | (checksum >> 56)) ^ message.Body[i];
                if (checksum != sentChecksum)
                    throw new MessageProtocolException(null, "Bad checksum in Ping message");
            }

            // send the message back to the client
            message.Name = "PingReply";
            agent.SendMessage(message);
            return;
        }

        // if the message is named "MessageElements", its body consists of a sequence of "message
        // elements" (XML elements) -- process them
        if (message.Name == "MessageElements")
        {
            using (MessageBuilder replyBuilder = new MessageBuilder())
			{
                try
                {
                    message.EnumerateMessageElements(delegate(XmlReader reader)
                    {
                        try
                        {
                            switch (reader.Name)
							{

							case "SyncNodeUpdate":

								var update = SyncNodeUpdate.Deserialize(reader);
                                ProcessSyncNodeUpdate(agent, update, replyBuilder);
								break;

                            case "SyncNodeCommand":

                                var command = SyncNodeCommand.Deserialize(reader);
                                ProcessSyncNodeCommand(agent, command, replyBuilder);
                                break;

                            default:

                                throw new SyncException("Invalid message element: {0}",
                                    reader.Name);
							}
                        }
                        catch (SyncException ex)
                        {
                            // higher-level error occurred -- e.g. SyncNodeUpdate had information
                            // that was invalid (e.g. device name not found, SyncPath invalid,
                            // wrong kind of value, etc.)
                            replyBuilder.Writer.WriteStartElement("Alert");
                            replyBuilder.Writer.WriteAttributeString("Type", "Warning");
                            string messageText = Util.FormatExceptionMessage(ex);
                            Util.Trace(true, "Warning: {0}", messageText);
                            replyBuilder.Writer.WriteAttributeString("Message", messageText);
                            replyBuilder.Writer.WriteEndElement();
                        }
                        catch (MessageProtocolException)
                        {
                            // lower-level error occurred -- e.g. invalid XML in message
                            // TODO: log (or at least trace)
                        }
                    });
                }
                catch (MessageProtocolException)
                {
                    // TODO: log (or at least trace): e.g. invalid message XML
                }

                // if there were any immediate reply message elements written to <replyBuilder> as
                // a result of processing <message>, send <replyBuilder> now
                Message replyMessage = replyBuilder.GetMessage();
                if (replyMessage != null)
                    agent.SendMessage(replyMessage);
            }
        }
    }

    // throws SyncException if device isn't found; returns null if it's not yet initialized or is disabled
    Driver GetDeviceFromSyncPath(SyncPath path)
    {
        // set <device> to the device specified in <pathParts>
        string deviceName = path.GetRequiredPart(0);
        Driver device;
        if (!_devices.TryGetValue(deviceName, out device))
            throw new SyncException("Device not found: {0}", deviceName);

        // ignore <update> if the device isn't initialized, or is disabled
        if (!device.IsEnabled)
        {
            Util.Trace("Ignored message because device \"{0}\" is disabled", device.Name);
            return null;
        }

        return device;
    }

    void ProcessSyncNodeUpdate(SyncClientAgent agent, SyncNodeUpdate update, MessageBuilder replyBuilder)
    {
        // set <device> to the device specified in <pathParts>, return if the device isn't initialized
        // or is disabled
        Driver device = GetDeviceFromSyncPath(update.Path);
        if (device == null)
            return;

        // give <update> to <device>
        try
        {
            Util.Trace("\"{0}\" device <-- Client#{1}: SyncNodeUpdate \"{2}\" -> {3}",
                device.Name, agent.Endpoint.ShortSessionId, update.Path, update.Value);
            device.ProcessSyncNodeUpdate(agent.Endpoint.SessionId, update, replyBuilder);
        }
        catch (SyncException ex)
        {
            throw new SyncException(ex, "\"{0}\" device: Error", device.Name);
        }
    }

    void ProcessSyncNodeCommand(SyncClientAgent agent, SyncNodeCommand command, MessageBuilder replyBuilder)
    {
        // set <device> to the device specified in <pathParts>, return if the device isn't initialized
        // or is disabled
        Driver device = GetDeviceFromSyncPath(command.Path);
        if (device == null)
            return;

        // give <command> to <device>
        try
        {
            Util.Trace("\"{0}\" device <-- Client#{1}: SyncNodeCommand \"{2}\": {3}",
                device.Name, agent.Endpoint.ShortSessionId, command.Path, command.Verb);
            device.ProcessSyncNodeCommand(agent.Endpoint.SessionId, command, replyBuilder);
        }
        catch (SyncException ex)
        {
            throw new SyncException(ex, "\"{0}\" device: Error", device.Name);
        }
    }

    internal void SendMessageElements(Guid sessionId, Action<MessageBuilder> del)
    {
        SyncClientAgent agent;
        if (_agents.TryGetValue(sessionId, out agent))
        {
            del(agent.OutgoingMessageElements);
            if (agent.OutgoingMessageElements.MayHaveMessage)
                SendAllOutgoingMessageElementsAsync();
        }
    }

    internal void SendMessageElements(IList<Guid> sessionIds, Action<Guid, MessageBuilder> del)
    {
        bool mayHaveMessage = false;
        foreach (Guid sessionId in sessionIds)
        {
            SyncClientAgent agent;
            if (_agents.TryGetValue(sessionId, out agent))
            {
                del(sessionId, agent.OutgoingMessageElements);
                mayHaveMessage = mayHaveMessage || agent.OutgoingMessageElements.MayHaveMessage;
            }
        }

        if (mayHaveMessage)
            SendAllOutgoingMessageElementsAsync();
    }

    internal void BroadcastMessageElements(Action<Guid, MessageBuilder> del)
    {
        bool mayHaveMessage = false;
        foreach (SyncClientAgent agent in _agents.Values)
        {
            del(agent.Endpoint.SessionId, agent.OutgoingMessageElements);
            mayHaveMessage = mayHaveMessage || agent.OutgoingMessageElements.MayHaveMessage;
        }

        if (mayHaveMessage)
            SendAllOutgoingMessageElementsAsync();
    }

    void SendAllOutgoingMessageElementsAsync()
    {
        if (_pendingSendAllOutgoingMessageElements == null)
        {
            _pendingSendAllOutgoingMessageElements = WorkQueue.Add(
                SendAllOutgoingMessageElements);
        }
    }

    WorkItem _pendingSendAllOutgoingMessageElements;

    class AgentMessagePair
    {
        public SyncClientAgent Agent;
        public Message Message;
    }

#if true
    Dictionary<string, List<AgentMessagePair>> _tempMessageMap = new Dictionary<string, List<AgentMessagePair>>();
#endif

    internal void SendAllOutgoingMessageElements()
    {
        _pendingSendAllOutgoingMessageElements = null;

#if true
        _tempMessageMap.Clear();
#endif
        // to reduce trace spew, group together messages that have equal trace output wo we can
        // display a single trace message for them
        foreach (SyncClientAgent agent in _agents.Values)
        {
            Message message = agent.OutgoingMessageElements.GetMessage();
            if (message != null)
            {
#if false
                // send the message -- let SendMessage do the tracing
                agent.SendMessage(message);
#elif false
                // equivalent to "agent.SendMessage(message)"
                Util.TraceBlock(message.BodyAsString, "Server --> Client#{0}", agent.Endpoint.ShortSessionId);
                agent.Endpoint.SendMessage(message);
#else
                string bodyAsString = message.BodyAsString;
                List<AgentMessagePair> agentMessagePairs;
                if (!_tempMessageMap.TryGetValue(bodyAsString, out agentMessagePairs))
                {
                    agentMessagePairs = new List<AgentMessagePair>();
                    _tempMessageMap.Add(bodyAsString, agentMessagePairs);
                }
                agentMessagePairs.Add(new AgentMessagePair()
                    {
                        Agent = agent,
                        Message = message
                    });
#endif
            }
        }

        foreach (KeyValuePair<string, List<AgentMessagePair>> kvp in _tempMessageMap)
        {
            string bodyAsString = kvp.Key;
            List<AgentMessagePair> agentMessagePairs = kvp.Value;

            Util.TraceBlock(bodyAsString, "Server --> Client#{0}",
                String.Join(",", agentMessagePairs.Select(
                    pair => pair.Agent.Endpoint.ShortSessionId.ToString()).ToArray()));

            foreach (AgentMessagePair pair in agentMessagePairs)
                pair.Agent.SendMessage(pair.Message);
        }
    }

	SyncClientAgent FindAgent(ServerEndpoint endpoint)
	{
		SyncClientAgent agent;
		_agents.TryGetValue(endpoint.SessionId, out agent);
		return agent;
	}

    internal static readonly Guid[] xxxPredefinedSessionIds = new Guid[]
    {
        new Guid("ac000001-7853-11de-b0bf-001742886b14"), // AgentId == 1
        new Guid("ac000002-7853-11de-b0bf-001742886b14")  // AgentId == 2
    };

    int _lastAgentId = xxxPredefinedSessionIds.Length;

    // allocate one more than <_lastAgentId>, unless <sessionId> is one of the predefined session IDs,
    // in which case the agent ID that's one more than its index in PredefinedSessionIds is returned
    internal int AllocateAgentId(Guid sessionId)
    {
        int agentId = 0;
        foreach (Guid predefinedSessionId in xxxPredefinedSessionIds)
        {
            ++agentId;
            if (predefinedSessionId == sessionId)
                return agentId;
        }

        return ++_lastAgentId;
    }

	SyncClientAgent CreateAgent(IServerEndpointBase endpoint)
	{
        var agent = new SyncClientAgent()
        {
            SyncServer = this,
            Endpoint = endpoint,
            //AgentId = AllocateAgentId(endpoint.SessionId)
        };
		_agents.Add(endpoint.SessionId, agent);
		return agent;
	}

    public DeviceParameters CreateDeviceParameters(string deviceName)
    {
        return new DeviceParameters()
        {
            SyncServer = this,
            Name = deviceName
        };
    }

    public void AddDevice(Driver device)
    {
        _devices.Add(device.Name, device);
    }

    internal bool AreAllDevicesEnabled()
    {
        foreach (Driver device in _devices.Values)
        {
            if (!device.IsEnabled)
                return false;
        }

        return true;
    }

    Guid _instanceId = Guid.NewGuid();

    public Message CreateClientFullUpdateMessage(Guid sessionId)
    {
        var builder = new StringBuilder();
        using (XmlWriter writer = Message.CreateMessageElementWriter(builder))
        {
            SyncNodeUpdate update = new SyncNodeUpdate()
            {
                Path = SyncPath.Build("InstanceId"),
                Value = XmlConvert.ToString(_instanceId)
            };
            update.Serialize(writer, false);


            foreach (Driver device in _devices.Values)
                device.SerializeStateForClient(sessionId, writer);
        }
        return new Message(builder);
    }

	void RequirePropertySetBeforeStart(string propertyName)
	{
		throw new InvalidOperationException(String.Format(
			"Property \"{0}\" must be set before Start has been called",
			propertyName));
	}

    void DisallowPropertySetAfterStart(string propertyName)
    {
#if !SILVERLIGHT
        if (_socketServer != null)
        {
            throw new InvalidOperationException(String.Format(
                "Property \"{0}\" may only be set before Start has been called",
                propertyName));
        }
#endif
    }

#if SILVERLIGHT
    internal SyncClientAgent InitializeSimulation(ServerEndpoint endpoint)
    {
        return CreateAgent(endpoint);
    }
#endif

    internal void BroadcastMessage(Message message)
    {
		// first, flush each SyncClientAgent.OutgoingMessageElements, if any
		SendAllOutgoingMessageElements();

		// now, send <message>
        foreach (SyncClientAgent agent in _agents.Values)
			agent.SendMessage(message);
    }

    internal ReadOnlySyncNode InitialDeviceStatus;

    // nested types...

    /* TODO: format: 
      SyncPath schema:
        -- "DeviceStatus\<device-name>" = true/false (indicating enabled state), or null if
           device cannot be enabled (i.e. initialization failed).
    */
    /// <summary>
    /// The driver for the built-in "DeviceStatus" device.
    /// </summary>
    ///
    internal class DeviceStatusDriver : Driver, ISingleInstanceBuiltInDriver
    {
        ///////////////////////////////////////////////////////////////////////////////////////////////
        // Device Class Overrides
        //

        protected override void OnEnable()
        {
            SyncServer.InitialDeviceStatus = PersistedTree;
            EnableCompleted(true);
        }

        public override void SerializeStateForClient(Guid sessionId, XmlWriter writer)
        {
            // make the "DeviceStatus" node have a value, to enable controls sync'd with it
            SerializeSyncNodeUpdate(writer, true, Name);

            foreach (Driver device in DeviceParameters.SyncServer._devices.Values)
            {
                // skip built-in devices
                if (device is ISingleInstanceBuiltInDriver)
                    continue;

                // serialize "DeviceStatus\<device-name>"
                SerializeDeviceStatus(device, null, writer, false);

                // serialize "Configuration\DeviceStatus\<device-name>"
                SerializeSyncNodeUpdate(writer, device.Name, "Configuration", "DeviceStatus", device.Name);
            }
        }

        public override void ProcessSyncNodeUpdate(Guid originator, SyncNodeUpdate update,
            MessageBuilder replyBuilder)
        {
            string deviceName = update.Path.GetRequiredLastPart(1);
            Driver device;
            if (!DeviceParameters.SyncServer._devices.TryGetValue(deviceName, out device))
                throw new SyncException(update, "Device \"{0}\" not found", deviceName);

            if (device is ISingleInstanceBuiltInDriver)
            {
                throw new SyncException(update, "Built-in device \"{0}\" cannot be enabled or disabled",
                    deviceName);
            }

            bool enable = update.GetTypedValue<bool>();

            // keep track of clients waiting for a response to this request
            StatusChangeOriginator sco;
            if (!device.StatusChangeOriginators.TryGetValue(originator, out sco))
            {
                sco = new StatusChangeOriginator()
                {
                    SessionId = originator
                };
                device.StatusChangeOriginators.Add(originator, sco);
            }
            sco.RequestsAreToEnable = enable;
            sco.RequestCount++;

            if (enable)
                device.Enable();
            else
                device.Disable();
        }

        // internal helper methods...

        internal static void SerializeDeviceStatus(Driver device, Guid? sessionId, XmlWriter writer, bool onlyBusyCount)
        {
            if (!(device is ISingleInstanceBuiltInDriver))
            {
                int originatedHere;
                if (sessionId != null)
                {
                    StatusChangeOriginator sco;
                    device.StatusChangeOriginators.TryGetValue(sessionId.Value, out sco);
                    if ((sco != null) && (sco.RequestsAreToEnable == device.IsEnabled))
                    {
                        originatedHere = sco.RequestCount;
                        device.StatusChangeOriginators.Remove(sessionId.Value);
                    }
                    else
                        originatedHere = 0;
                }
                else
                    originatedHere = 0;

                var deviceStatusPath = SyncPath.Build("DeviceStatus", device.Name);

                SyncNodeUpdate update;

                if (!onlyBusyCount)
                {
                    update = new SyncNodeUpdate()
                    {
                        Path = deviceStatusPath,
                        Value = device.CanEnable ? (object)device.IsEnabled : null,
                        OriginatedHere = originatedHere
                    };
                    update.Serialize(writer, false);
                }

                update = new SyncNodeUpdate()
                {
                    Path = SyncPath.Build(deviceStatusPath, "BusyCount"),
                    Value = device.BusyCount
                };
                update.Serialize(writer, false);
            }
        }
    }

    // implements the fake client that sits on the server; used to allow a driver to behave like a
    // client for the purpose of interacting with other drivers
    internal class PseudoClientImplementation : ISyncClient
    {
        SyncServer _syncServer;

        SyncNode _syncTree;

        internal IServerEndpointBase ServerEndpointBase { get; private set; }
        internal SyncClientAgent Agent { get; set; }

        /// <summary>
        /// The <r>SyncNode.DescendantChanged</r> events that need to be fired.  This collection is
        /// updated by <r>SyncNode.SetValue</r>, and the events are fired asynchronously (so that
        /// multiple events from the same event sender (<r>SyncNode</r>) can be collapsed into one).
        /// </summary>
        Dictionary<SyncNode, DescendantChangedEventArgs> _descendantsChangedQueue =
            new Dictionary<SyncNode, DescendantChangedEventArgs>();

        public PseudoClientImplementation(SyncServer syncServer)
        {
            _syncServer = syncServer;
            _syncTree = SyncNode.CreateTree(this);
            ServerEndpointBase = new ServerEndpointBase(ShortSessionIdManager.PredefinedSessionIds[0],
                SendMessageToClient);
        }

        void SendMessageToClient(Message message)
        {
            // if the message is named "MessageElements", its body consists of a sequence of "message
            // elements" (XML elements) -- process them
            if (message.Name == "MessageElements")
            {
                try
                {
                    message.EnumerateMessageElements(delegate(XmlReader reader)
                    {
                        try
                        {
                            SyncNode syncNode;
                            switch (reader.Name)
                            {

                            case "SyncNodeUpdate":

                                // this message element is a notification that a SyncNode changed; the
                                // change may have originated from this client, another client, or
                                // multiple clients (in the case of a merged update)
                                var update = SyncNodeUpdate.Deserialize(reader);
                                syncNode = (SyncNode)_syncTree.CreateNode(update.Path);
                                syncNode.OnServerReportedValueChange(update.Value, update.OriginatedHere);
                                break;

                            case "SyncNodeCommand":

                                // this message element is a notification that a SyncNodeCommand that
                                // originated from this client has completed
                                var command = SyncNodeCommand.Deserialize(reader);
                                syncNode = (SyncNode)_syncTree.CreateNode(command.Path);
                                syncNode.OnServerReportedOperationCompletion(command.OriginatedHere);
                                break;

                            }
                        }
                        catch (MessageProtocolException)
                        {
                            // TODO: log (or at least trace): poorly formatted message elements
                        }
                    });
                }
                catch (MessageProtocolException)
                {
                    // TODO: log (or at least trace): e.g. invalid message XML
                }
            }
        }

        public SyncNodeBase SyncTree
        {
            get
            {
                return _syncTree;
            }
        }

        public bool IsOnline
        {
            get
            {
                return true;
            }
        }

        public event IsOnlineChangedEventHandler IsOnlineChanged
        {
            // we never fire this event
            add
            {
            }
            remove
            {
            }
        }

        public void QueueSyncNodeToSendToServer(SyncNodeBase node)
        {
            var replyBuilder = new MessageBuilder();
            var update = new SyncNodeUpdate()
            {
                Path = node.Path,
                Value = node.Value
            };
            _syncServer.ProcessSyncNodeUpdate(Agent, update, replyBuilder);

            Message reply = replyBuilder.GetMessage();
            if (reply != null)
                SendMessageToClient(reply);
        }

        public void QueueSyncNodeCommandToSendToServer(SyncNodeBase node, string verb)
        {
            var replyBuilder = new MessageBuilder();
            var command = new SyncNodeCommand()
            {
                Path = node.Path,
                Verb = verb
            };
            _syncServer.ProcessSyncNodeCommand(Agent, command, replyBuilder);

            Message reply = replyBuilder.GetMessage();
            if (reply != null)
                SendMessageToClient(reply);
        }

        public void QueueDescendantChangedEvent(SyncNodeBase syncNode, DescendantChanges changes, object originator)
        {
            DescendantChangedEventArgs eventArgs;
            if (!_descendantsChangedQueue.TryGetValue((SyncNode)syncNode, out eventArgs))
            {
                eventArgs = new DescendantChangedEventArgs()
                {
                    Changes = changes,
                    Originator = originator
                };
                _descendantsChangedQueue.Add((SyncNode)syncNode, eventArgs);

                if (_processDescendantsChangedQueueWorkItem == null)
                    _processDescendantsChangedQueueWorkItem = _syncServer.WorkQueue.Add(
                        ProcessDescendantsChangedQueue);
            }
            else
            {
                eventArgs.Changes |= changes;
                if (eventArgs.Originator != originator)
                    eventArgs.Originator = null; // event has multiple originators
            }
        }

        WorkItem _processDescendantsChangedQueueWorkItem;

        void ProcessDescendantsChangedQueue()
        {
            _processDescendantsChangedQueueWorkItem = null;

            foreach (KeyValuePair<SyncNode, DescendantChangedEventArgs> kvp in
                _descendantsChangedQueue)
            {
                kvp.Key.FireDescendantChangedEvent(kvp.Key, kvp.Value, kvp.Value.Originator);
            }
            _descendantsChangedQueue.Clear();
        }
    }
}

/// <summary>
/// TODO
/// </summary>
interface ISingleInstanceBuiltInDriver
{
}

/// <summary>
/// The representative of a specific HomeUX client on this server.
/// </summary>
///
/// <remarks>
/// When a HomeUX client connects to this HomeUX server, a TODO-ServerEndpoint-??? is created
/// for communicating with the client.  Once the initial handshake is completed and the client
/// has provided a session ID (a GUID identifying the client session), the server looks for
/// an existing <r>SyncClientAgent</r> (in the case where the client previously connected an
/// the connection was temporarily lost) or else creates a new <r>SyncClientAgent</r>.
/// </remarks>
///
[DebuggerDisplay("{DebuggerDisplay}")]
internal class SyncClientAgent
{
    public SyncServer SyncServer { get; set; }
    public IServerEndpointBase Endpoint { get; set; }

    public MessageBuilder OutgoingMessageElements { get; private set; }

    public SyncClientAgent()
    {
        OutgoingMessageElements = new MessageBuilder();
    }

    public void SendMessage(Message message)
    {
        Util.TraceBlock(message.BodyAsString, "Server --> Client#{0}", Endpoint.ShortSessionId);
        Endpoint.SendMessage(message);
    }

    //public SyncClientAgent(ServerEndpoint endpoint)
    //{
    //    Endpoint = endpoint;
    //}

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Properties
    //

    /// <summary>
    /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
    /// from displaying it in quotes.
    /// </summary>
    object DebuggerDisplay
    {
        get
        {
			if (Endpoint != null)
			{
				return new EncapsulatedString("SyncClientAgent for Client#{0} ({1})",
					Endpoint.ShortSessionId, Endpoint.SessionId);
			}
			else
				return "SyncClientAgent";
        }
    }
}

/// <summary>
/// TODO
/// </summary>
public interface IServerEndpointBase
{
    Guid SessionId { get; }
    int ShortSessionId { get; }
    bool SendMessage(Message message);
}

internal static class ShortSessionIdManager
{
    internal static readonly Guid[] PredefinedSessionIds = new Guid[]
    {
        new Guid("ac000001-7853-11de-b0bf-001742886b14"), // ShortSessionId == 1
        new Guid("ac000002-7853-11de-b0bf-001742886b14")  // ShortSessionId == 2
    };

    static int s_lastShortSessionId = PredefinedSessionIds.Length;

    // allocate one more than <_lastShortSessionId>, unless <_sessionId> is one of the predefined
    // session IDs, in which case the short session ID that's one more than its index in
    // PredefinedSessionIds is returned
    public static int AllocateShortSessionId(Guid sessionId)
    {
        int shortSessionId = 0;
        foreach (Guid predefinedSessionId in PredefinedSessionIds)
        {
            ++shortSessionId;
            if (predefinedSessionId == sessionId)
                return shortSessionId;
        }

        return System.Threading.Interlocked.Increment(ref s_lastShortSessionId);
    }
}

/// <summary>
/// TODO
/// </summary>
[DebuggerDisplay("ServerEndpointBase for Client#{_shortSessionId} ({_sessionId})")]
public class ServerEndpointBase : IServerEndpointBase
{

    //----

    Guid _sessionId;
    int _shortSessionId;
    Action<Message> _sendMessage;

    public ServerEndpointBase(Guid sessionId, Action<Message> sendMessage)
    {
        _sessionId = sessionId;
        _shortSessionId = ShortSessionIdManager.AllocateShortSessionId(sessionId);
        _sendMessage = sendMessage;
    }

    public Guid SessionId
    {
        get
        {
            return _sessionId;
        }
    }

    public int ShortSessionId
    {
        get
        {
            return _shortSessionId;
        }
    }

    public bool SendMessage(Message message)
    {
        _sendMessage(message);
        return true;
    }
}

#if SILVERLIGHT
// simulated ServerEndpoint
public class ServerEndpoint : ServerEndpointBase
{
    public ServerEndpoint(Guid sessionId, Action<Message> sendMessage) :
        base(sessionId, sendMessage)
    {
    }
}
#endif

/// <summary>
/// The driver for the built-in "Personalization" device.
/// </summary>
///
class PersonalizationDriver : Driver, ISingleInstanceBuiltInDriver
{
    Dictionary<SyncPath,object> _values = new Dictionary<SyncPath,object>();

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Device Class Overrides
    //

    protected override void OnInitialize()
    {
        WalkTree(PersistedTree);
    }

    void WalkTree(SyncNodeBase syncNode)
    {
        if (syncNode.Value != null)
            _values.Add(syncNode.Path, syncNode.Value);
        foreach (SyncNodeBase childNode in syncNode.Children)
            WalkTree(childNode);
    }

    public override void SerializeStateForClient(Guid sessionId, XmlWriter writer)
    {
        var update = new SyncNodeUpdate();
        foreach (KeyValuePair<SyncPath, object> kvp in _values)
        {
            update.Path = kvp.Key;
            update.Value = kvp.Value;
            update.Serialize(writer, false);
        }
    }

    public override void ProcessSyncNodeUpdate(Guid originator, SyncNodeUpdate update,
        MessageBuilder replyBuilder)
    {
        _values[update.Path] = update.Value;
        BroadcastSyncNodeOperation(update, new List<Guid>() { originator });
        // TODO: persist <_values> (on server, and in isolated storage for simulator)
    }
}

}
